Android IPC简介
IPC 是 Inter-Process Communication 的缩写,含义为 进程间通信或者跨进程通信,是指两个进程之间进行数据交换的过程。
a
IPC 不是 Android 所独有的,任何一个操作系统都需要有相应的 IPC 机制。
在 Android 中最有特色的进程间通信方式就是 Binder 了, 通过 Binder 可以轻松实现进程间通信。
除了 Binder ,Android 还支持 Socket, 通过 Socket 也可以实现任意两个终端之间的通信,当然一个设备的两个进程之间的也可以通过 Socket 进行通信。
多进程的情况分两种:
- 一个应用因为某些原因自身需要采用多进程模式来实现
- 可能的原因如下:
- 有些模块需要运行在单独的进程中
- 加大一个应用可使用的内存
- 可能的原因如下:
- 当前应用需要向其他应用获取数据。
2 Android中的多进程模式
通过给四大组件指定 android:process属性,可以轻易地开启多线程模式。
1 开启多进程模式
一般地,在 Android 中多进程是指一个应用中存在多个进程的情况(此处不讨论两个应用之间的多进程情况)
首先,在 Android 中使用多进程只有一种方法, 在 manifest 文件中,给四大组件 指定 android:process 属性。
- 其实还有一种非常规的多进程方法—— 通过 JNI 在 native 层去 fork 一个新的进程。
根据 process 的属性值不同,创建不同的进程。
问题:使用 「:xxx」与 使用 「xxx.xxx.xxx」两种方式有区别吗?
- 「:」的含义是指在当前的进程名前面附上当前的包名,即 这是一种简写的方法
- 进程名以「:」开头的进程属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中,
- 而进程名不以「:」开头的进程属于全局进程,其他应用通过 ShareUID 方式可以和它跑在同一个进程中。
Android 系统会为每个应用分配一个 唯一的 UID,具有相同 UID 的应用才能共享数据。
- 两个应用跑在同一个进程中 需要
- 这两个应用有 相同的 shareUID
- 并且 签名相同。
- 这种情况下,可以互相访问对方的私有目录,如 data 目录。
2 多进程模式的运行机制
==一个进程 = 一个 虚拟机==
Android 为每个应用分配了一个独立的虚拟机,或者说为每个进程分配一个独立的虚拟机,
不同虚拟机在内存分配上有不同的地址空间,这就导致了不同虚拟机范文同一个类的对象会产生多份副本。
一般来说,使用多进程会造成以下几方面问题
- 静态成员和单例模式完全失效
- 线程同步机制完全失效
- SharePreferences 的可靠性下降,好像有多进程模式?有,但是不稳定,所以系统已经不推荐使用了。
- Application 会多次创建
可以这样理解同一个应用间的多进程:
- 它就相当于两个不同的应用采用了 ShareUID 的模式,这样能够更加直接地理解多进程模式的本质。
3 IPC基础概念介绍
3.1 Serializable接口
Serializable接口 :Java 所提供的一个序列化接口,它是一个空接口,为对象提供标准的序列化和反序列化操作。
使用:
- 只要在类的声明中指明一个类似下面的标识即可自动实现 *默认的* 序列化过程(可以自定义)。
- `private static final long serialVersionUID = 87138828109787189L;`
这个 serialVersionUID 可以没有,但是这样会对反序列化过程产生影响。
serialVersionUID是用来辅助序列化过程的,原则上序列化后的数据中的serialVersionUID只有和当前类的serialVersionUID相同才能正常地被反序列化。
serialVersionUID 的详细工作机制:
- 序列化是把当前类的
serialVersionUID写入序列化的文件中(也可能是其他中介)当反序列化时会去检测文件中的serialVersionUID,看它是否和当前类的serialVersionUID一致,如果一致就说明序列化的版本与当前类的版本是相同的,这时可以成功反序列化。 - 否则说明当前类和序列化的类相比发生了 某些变化,例如:成员变量的数量、类型可能发生了改变,这时无法正常序列化,会报如下错误:1Exception in thread "main" java.io.InvalidClassException: projectname.clasname; local class incompatible: stream classdesc serialVersionUID = -6009442170907349114, local class serialVersionUID = 6529685098267757690
一般而言,我们应该手动指定 serialVersionUID 的值,比如 1L,
也可以让 Eclipse 根据当前类的的结构去自动生成它的 hash 值,这样序列化和反序列化时两者的 serialVersionUID是相同的,可以正常进行反序列化。
如果类的结构发生了改变,比如增加/删除了 某些成员变量,那么系统就会重新计算当前类的 hash 值并把它赋值给 serialVersionUID ,这个时候当前类的 serialVersionUID 就会和序列化数据不一致,于是反序列化失败。
- 如果手动指定了
serialVersionUID就不会有这个问题。
但是如果类结构发生了 ==非常规性改变==,比如修改了类名、修改了成员变量类型,这个时候尽管 serialVersionUID 验证通过了,但是反序列化过程还是会失败,因为类结构发生了毁灭性变化,根本无法从老版本的数据中还原出一个新的类结构的对象。
综上,类结构不变且类的版本不变(没有增加或者删除成员变量)的情况下,不指定 serialVersionUID 是没有问题的,但是为了提高稳定性,还是要指定。
两点要注意的:
- 静态成员变量属于类不属于对象,所以不会参与序列化过程。
- 用 transient 关键字标记的 成员变量不参与序列化过程。
系统的默认序列化过程也是可以改变的。只要重新实现 writeObject 和 readObject 方法即可。
如何进行对象的序列化和反序列化?
采用 ObjectOutputStream ObjectInputStream 即可轻松实现。
3.2 Parcelable接口
通过实现Parcelable接口序列化对象的步骤:
1、声明实现接口Parcelable
2、实现Parcelable的方法 writeToParcel,将你的对象序列化为一个Parcel对象
3、实例化静态内部对象CREATOR实现接口Parcelable.Creator ,实现 反序列化
Parcelable 的方法说明
| 方法 | 功能 | 标记位 | |
|---|---|---|---|
| createFromParcel | 从序列化后的对象中创建原始对象 | ||
| newArray | 创建指定长度的原始对象数组 | ||
| User(Parcel in) | 从序列化后的对象中创建原始对象 | ||
| writeToParcel(Parcel dest, int flags) | 将当前的对象写入序列化结构中,其中 flags 标识有两种值,0 或者 1; 为 1 时标识当前对象需要作为返回值返回,不能立即释放资源,几乎所有情况都为 0 | PARCELABLE_WRITE_RETURN_VALUE | |
| describeContents | 返回当前对象的描述,仅当当前对象中存在 文件描述符,才返回 1,几乎所有情况都返回 0 | CONTENT_FILE_DESCRIPTOR |
新建一个类,实现 Parcelable 接口,然后写好成员变量,再 alt + enter 自动补全代码。
对比
- Serializable 是 Java 中的序列化接口,使用起来简单但是==开销很大==,序列化和反序列化需要大量的 I/O 操作。
- 而 Parcelable 是 Android 中序列化方式,更适合使用在 Android 平台上,==效率高==。 缺点:使用起来麻烦。
- Parcelable 主要用在内存序列化上
- 通过 Parcelable 将对象序列化到存储设备中 或者将对象序列化后存储通过网络传输也都是可以的,但是这个过程会稍显复杂,在这两种情况下,建议使用 Serializable 接口。
3.3 Binder
直观来说,Binder 是 Android 的一个类,它实现了 IBinder 接口。
- 从 IPC 角度来说, Binder 是 Android 中的一种跨进程通信方式
- Binder 还可以理解为一种虚拟的物理设备,它的设备驱动是 /dev/binder, 该通信方式在 Linux 上没有。
- 从 Android FrameWork 角度来看, Binder 是 ServiceManager 连接各种 Manager (ActivityManager、 WindowManager, 等等)和相应的 ManagerService 的桥梁
- 从 Android 应用层 来说,Binder 是客户端和服务端进行通信的媒介,当 bindService 的时候,服务端会返回一个包含了服务端业务调用的 Binder 对象,通过这个 Binder 对象,客户端就可以获取服务端提供的数据或服务(包括普通的服务 和 AIDL服务)
Android 开发中,Binder 主要用在 Service 中,包括 AIDL 和 Messenger,
- 普通 Service 中的 Binder 不涉及进程间通信,所以较为简单,无法涉及核心
- Messenger 的底层其实是 AIDL
示例中,编写代码时,查找 aidl 生成的接口,Project ==》app==》build ==》source==》debug==》com.xxx.xx.xx==》 目标类
- ==注意==:AIDL 的特殊之处:尽管两个类都位于同一个包中,还是需要导入相应的类。
==所有==可以在 Binder 中传输的接口都需要继承自 IInterface 接口。
栗子
4 Android中的IPC方式
4.1 使用Bundle
四大组件中的三大组件(Activity、Service、Receiver)都是支持在 Intent 中传递 Bundle 数据的,Bundle 实现了 Parcelable 接口,所以它可以方便地在不同的进程间 传输。
因此,当我们在一个进程中启动了另一个进程的 Activity、Service、Receiver,我们就可以在 Bundle 中附加我们需要传输给远程进程的信息并通过 Intent 传递出去。
- 当然我们所传输的数据必须能够被序列化,如:
- 基本数据类型
- 实现了 Parcelable 接口的对象。
- 实现了 Serialiable 接口的对象。
- 一些 Android 支持的特殊对象。
- Charserquence
- StringArrayList
- IntegerArrayList
- Size
- …
特殊的使用场景:
进程 A 要进行计算,并将计算结果发送给进程 B,但是该计算结果不支持放入 Bundle 中。
- 解决:在 A 中,通过 Intent 来启动线程 B 的一个 Service 组件(比如 IntentService),让 Service 在后台计算,计算完以后再启动 B 进程中想要启动的目标组件。
核心思想:将原本需要在 A 进程的计算任务转移到 B 进程的服务中,这样就避免了进程间通信的问题,而且代价也不大。
4.2 使用文件共享
两个进程通过读/写同一个文件交换数据。
- 比如,进程 A 写入,进程 B 读取。
背景知识:
- Window 上,一个文件如果被加了排斥锁将会导致其他线程无法对其进行访问(包括读和写)。
- Android 基于 Linux,其读/写文件可以无限制地进行,甚至两个线程对同一个文件并行写也是允许的。(尽管这可能会出现一些问题)
应用:
- 序列化一个对象到文件系统中的同时,从另一个进程中恢复这个对象。
- 另一个进程成功地恢复之前存储的对象的内容,但是他们本质还是两个对象。
可能存在的问题:
并发读/写,读出的内容可能不是最新的。
- 解决:
- 设法避免
- 考虑使用线程同步来限制,多个线程的写操作。
文件共享适合对数据同步要求不高的进程之间进行通信。
Sharepreference
SharePreference 是个特例
底层通过 xml 文件的方式来存储键值对,,一般而言,它位于 /data/data/当前应用的包名/share_prefs 目录下,
由于系统对 SharePreference 的读写操作有一定的缓存策略,即在内存*有一份 SharePreferce 的缓存。在多进程模式下,系统对它的读写操作就变得不可靠。高并发状态下,SharePreference 很大几率会丢失数据。
- 不建议在进程间通信使用 SharePreference
4.3 使用Messenger
『信使』
通过它可以在不同进程中传递 Message 对象,在 Message 中放入我们需要传递的数据,就可以实现进程间的数据传递了。
Messenger 一次处理一个请求,所以服务端不用考虑线程同步的问题(因为不存在并发执行的情形)。
1. 服务端进程
客户端进程
在 Messager 中进行数据传递必须将数据放入 Messenger 中,
- Message 中支持数据类型就是 Messenger 所支持的传输类型
- Message 中所能使用的载体只有
- int what;
- int ar1
- int arg2
- Messenger replyTo
- Object obj
- 2 之前 obj 字段不支持跨进程传输
- 2 之后 obj 也仅系统提供的实现 Parcelable 接口的的对象
- 即 FrameWork class
- Bundle
- 还好我们有 Bundle ,支持比较多的数据类型
如果要让『服务端』能够回复信息。
Android 接口定义语言(Android Interface Definition Language)
Messenger 只能传递消息,不能跨进程调用方法。而且只能串行处理客户端发来的请求。
- 虽然它底层也是使用 AIDL 实现的。
可以使用 AIDL 来实现跨进程的方法调用。
aidl 文件的作用是 sdk 根据它来生成相应的 java 代码,也就是在编译时有用,在编译完之后,即使删除掉 aidl 文件,也是可以的,但是这样的话如果重新编译就没法生成 java 代码了。
将编写的 .aidl 文件保存在项目的 src/ 目录内,当你开发应用时,SDK 工具会在项目的 gen/ 目录中生成 IBinder 接口文件。生成的文件名与 .aidl 文件名一致,只是使用了 .java 扩展名(例如,IRemoteService.aidl 生成的文件名是 IRemoteService.java)。
如果使用 Android Studio,增量编译几乎会立即生成 Binder 类。 如果不是使用 Android Studio,则 Gradle 工具会在下一次开发应用时生成 Binder 类
通常应该在编写完 .aidl 文件后立即用 gradle assembleDebug (或 gradle assembleRelease)编译项目,以便您的代码能够链接到生成的类。
1. 服务端
用一个 Service 来监听客户的连接请求,创建一个 AIDL 文件,将暴露给客户端的接口在这个 AIDL 文件中声明,最后在 Service 实现该接口。
2. 客户端
- 绑定服务端 Service,
- 绑定成功后将服务端返回的 IBinder 转化为 AIDL 接口所属的类型
- 注意不是平时那样强转,而是
IBookManager.Stub.asInterface(iBinder);
- 注意不是平时那样强转,而是
- 接着就可以使用 AIDL 中的方法了。
AIDL 支持的数据类型:
- 基本数据类型
- CharSequence
- List 只支持 ArrayList
- Map 只支持 HashMap
- Parcelable
AIDL:AIDL 接口本身也可以在 AIDL 文件中使用
- AIDL 中无法使用普通的接口
注意:
- 自定义的 Parcelable对象一定要显式地 import 进来。(不管它们是否与当前的 AIDL 文件位于同一个包中)
- 如果 AIDL 文件中用到了自定义的 Parcelable 对象,必须使用新建一个和它同名的 aidl 文件,并在其中声明它为 parcelable 类型
- 如 Book.java
- Book.aidl
- 要在其中声明 pacelable Book;
- Book.aidl
- 如 Book.java
基本数据类型以外的参数需要==标上方向==
- in 输入型参数,类似于普通方法的参数
- out 输出型参数, 类似于返回值
- inout 输入输入型参数
所以说,输入输出是针对这个方法而言的,而不是 C/S 结构中的输入/输出。
AIDL 接口中只支持方法,不支持声明静态变量(==跟传统的接口有区别==)
- AIDL 包结构在客户端与服务端要保持一致,否则运行会出错。
- 因为客户端需要反序列化服务端中和 aidl 接口相关的类。
AIDL 接口中所支持的是抽象的 List ,而 List 只是一个接口,虽然服务端使用 CopyOnWriteArrayList ,但是在 Binder 中会按照 List 的规范去访问数据并最终形成一个 ArrayList 传递给客户端。
- ConcurrentHashMap 同理
在服务端调用客户端(注册时的使用的接口中)的方法,在客户端的 Binder 线程池中执行。
对象是不能直接跨进程传输的,对象的跨进程传输本质上都是反序列化过程
RemoteCallBackList 是系统专门用来发删除跨进程的 listener 的接口。
- 它是一个泛型,支持管理任意的 AIDL 接口
public class RemoteCallBackList<E extends IInterface>- 当客户端进程终止之后, RemoteCallBackList 能够自动溢出客户端所注册的 listener
- 其内部有一个
ArrayMap<IBinder,CallBack>用来专门保存所有的 AIDL 回调- CallBack 封装了真正的远程 listener。当客户端注册 listener 的时候,会将其中的信息存入 mCallBack 中12IBinder key = listener.asBinder();callBack value = new Callback(listener,cookie);
- CallBack 封装了真正的远程 listener。当客户端注册 listener 的时候,会将其中的信息存入 mCallBack 中
多次跨进程传输客户端的同一个对象会在服务端生成不同的对象,但是这些新生成的对象有一个 共同点,那就是==它们底层的 Binder 对象是同一个==
==注意==:
- RemoteCallBackList 使用方式比较特别。并不像 list 那样
- beginBroadcast()
- finishBroadcast()
- 这两个方法==一定要配对使用==
- getBroadcastItem()12345678final int N = mCallBackList.beginBroadcast();for (int i = 0; i < N; i++) {IOnNewBookArrivedListener l = mCallBackList.getBroadcastItem(i);if (l != null) {l.onNewBookArrived(book);}}mCallBackList.finishBroadcast();
客户端的 onServiceConnected 和 onServiceDisconnected(ComponentName name)都执行在 UI 线程中,不可以在里面调用服务端耗时的方法。
服务端方法本身运行在服务端的 Binder 线程池中,所以服务端方法本身即可执行大量耗时操作。
- 不要在服务端方法中开线程去执行异步任务。
服务端也有可能运行在 UI 线程,这时要尽量避免调用耗时方法。
Binder 死亡后
进程空间分为用户空间和内核空间
- 用户空间,数据互相隔离
- 内核空间 数据共享
通过内核 实现跨进程调用
进程 A 调用进程 B 的函数
- 知道调用的是哪一个对象的哪一个方法
- 传递方法参数
- 进程 B 执行完之后,返回相应的数据给进程 A。
本质上就是 数据的传递。
客户端调用的进程 Binder 对象的引用(一个代理),实际执行还是在服务端的。
Client 进程的的操作实际上对代理对象的操作,代理对象利用 Binder 驱动找到真正的 Binder对象,并通知 Server 进程完成操作。
采用的是 C/S 的架构。


4.5 使用ContentProvider
待补充
4.6 使用Socket
待补充
参考资料与学习资源推荐
- 《Android开发艺术探索》
由于本人水平有限,可能出于误解或者笔误难免出错,如果发现有问题或者对文中内容存在疑问欢迎在下面评论区告诉我,请对问题描述尽量详细,以帮助我可以快速找到问题根源。谢谢!
##